
/*
 * Negotiating GObject reference counting in C/Matlab
 *
 * Here, "proxy" refers to the Matlab container for a GObject and "object"
 * refers to the C version of the GObject.  Proxies are handle-type objects
 * in Matlab, meaning that copying merely increases the Matlab reference
 * count.  They merely store a pointer to the C object (as an integer) for
 * passing to C functions.
 *
 * Every GObject may have no more than one proxy.  A proxy must hold a C
 * reference to its object to ensure that the object exists as long as the
 * proxy does.  While there are other C references to the object, the object
 * must hold a strong reference to the proxy.  When all other references to
 * the object (except the proxy's) have been dropped, the object's reference
 * to the proxy becomes weak, meaning that it should not prevent the proxy
 * from being destroyed.  If an object holds only a weak reference to its
 * proxy and all Matlab references to the proxy are dropped, the proxy can
 * be destroyed, which should release the final reference to the object.
 *
 * Unfortunately, Matlab does not support the notion of weak references.
 * To get around this, we store a copy of the proxy in the object's qdata.
 * That way, even if all other Matlab references are dropped, one last
 * reference remains "hidden" in the object.  When all other C references
 * are dropped, the cached copy of the proxy is released.  If there are
 * other Matlab references, the object is said to have a "weak reference"
 * to the proxy (even though the object has no actual way to access the
 * proxy).  If, on the other hand, there are no other Matlab references when
 * the cached copy is released, the Matlab garbage collector will destroy
 * the proxy, which should release the final reference to the object.
 *
 * To enable the release of cached proxies, the proxy's reference to the
 * object is a toggle reference.  Toggle references execute a callback when
 * they are the last reference to an object.  In this case, the callback
 * releases the cached proxy.
 *
 * Matlab must locate a proxy for any objects that are returned from calls
 * to C functions.  This is referred to as "wrapping." To wrap an object,
 * first the object's qdata is checked for a cached proxy.  If none exists,
 * a new Matlab proxy is created, a toggle reference is set for the object,
 * and the proxy is cached in the object.
 *
 * When a C function called from Matlab returns a new reference to some
 * object, that is, the function increases the object's reference count and
 * expects the caller to take responsibility for unreferencing the object
 * later, Matlab should unreference the object after wrapping it, since
 * Matlab keeps one and only one reference to any objects it knows about
 * (namely, the proxy's toggle reference).  While this is most applicable to
 * g_object_new() and object constructors, bindings authors should take note
 * and write a Matlab override for other functions that behave similarly.
 *  ** TODO: Rewrite this concerning transfer notation **
 * 
 * Once created, a proxy persists for the life of its object, though the
 * object may not always be aware of the proxy.  Objects are unware of their
 * proxies only when there are no C references to the object (other than the
 * proxy's).  Consequently, no C code will ever need to access the proxy of
 * an object when the object is ignorant of the existence of the proxy.
 *
 * When Matlab calls a C function with a wrapped object, there is a
 * possibility that the C function could increase the object's reference
 * count, requiring that the object hold a strong reference to its proxy. 
 * But, objects with only weak references to their proxy do not actually
 * have access to their proxy object.  So, before each C call, Matlab must
 * ensure that any GObject arguments have stored a copy of their proxy. 
 * This entails caching the proxy and increasing the C reference count. 
 * When the call is complete, Matlab unreferences the object (after wrapping
 * any returned objects).  This is a bit clumsy, but ensures that C objects
 * can always return their proxy as long as it exists while still allowing
 * Matlab to dispose of unneeded proxies.
 *
 * To summarize:
 * - g_object_matlab_wrap() on returned object: return cached proxy, or:
 *     create new proxy, g_object_add_toggle_ref(), store proxy
 * - matlab passes object to C function: g_object_ensure(), which caches
 *   proxy (if ncessary) and increases ref count, with unref after call
 * - object is unref'd so that proxy holds last remaining reference:
 *     remove any cached proxy
 * - matlab destroys proxy: g_object_matlab_remove_toggle_ref()
 * - function transfers ref to caller (e.g. g_object_new): wrap, then
 *     unref the sender's reference
 */

#include <mex.h>
#include "gmatlab.h"

static void g_object_matlab_toggle_notify(gpointer data, GObject *obj,
                                          gboolean is_last_ref);

GQuark matlabWrapperKey = 0;

static GSList *cleanup = NULL;
static GSList *escape = NULL;

/**
 * g_matlab_cleanup_add():
 * @func: cleanup function
 * @data: (allow-none): data to clean up
 *
 * Adds the callback @func to clean up @data when matlab calllib() is
 * complete or when a matlab error is thrown.  This constitutes a very basic
 * kind of garbage collection.  In liue of freeing memory or unreferencing
 * pointers directly, use g_matlab_cleanup_add() to delay the cleanup until
 * the matlab C-call ends, either naturally or due to error.  Should not be
 * called on memory that needs to persist beyond the current call.
 *
 * Does nothing if @data is %NULL.
 *
 * Returns: @data
 */
gpointer g_matlab_cleanup_add(GHookFunc func, gpointer data)
{
  if (data == NULL) return NULL;
  g_return_val_if_fail(func != NULL, NULL);
  if (cleanup == NULL) g_matlab_cleanup_push();
  GHook *hook = g_hook_alloc(cleanup->data);
  hook->func = func;
  hook->data = data;
  g_hook_prepend(cleanup->data, hook);
  return data;
} 

/**
 * g_matlab_cleanup:
 *
 * Executes all of the cleanup callbacks set by g_matlab_cleanup_add() since
 * the last call to g_matlab_cleanup() in <emphasis>reverse</emphasis>
 * order.  Should be called at the end of every entry function from matlab.
 *
 * This also executes the emergency escape callbacks, if any exist.  You
 * must call g_matlab_escape_clear() <emphasis>before</emphasis>
 * g_matlab_cleanup() at the end of an function that uses
 * g_matlab_escape_add().  Emergeny escape callbacks are called before the
 * regular cleanup callbacks.
 */
void g_matlab_cleanup()
{
//  TRACE("g_matlab_cleanup()");
  if (cleanup == NULL) return;
  g_matlab_escape_init();
  g_hook_list_invoke(cleanup->data, FALSE);
  g_hook_list_clear(cleanup->data);
  g_hook_list_init(cleanup->data, sizeof(GHook));
}

/**
 * g_matlab_escape_init:
 *
 * Some memory is allocated to be transferred to a permanent data structure.
 * If there is an error during the C call, this memory will be leaked.  But,
 * we don't want to add it to the g_matlab_cleanup() list, since we don't
 * usually want to free the memory at the end of the call---only in the event
 * of an error.  To accommodate this, there is a parallel list of cleanup 
 * hooks to be used only if the a call sequence is interrupted.  "Protected"
 * regions of code are enclosed in calls to g_matlab_escape_init() and
 * g_matlab_escape_clear().  After a call to g_matlab_escape_init(), if
 * there is a second call to g_matlab_escape_init() or g_matlab_cleanup()
 * <emphasis>without</emphasis> a call to g_matlab_escape_clear(), the
 * previous call is deemed to have been interrupted, and the escape callbacks
 * are invoked.
 *
 * In brief, use g_matlab_escape_add() to install an emergency cleanup
 * callback to prevent leaks in the event that a call cannot be completed.
 * Enclose regions of code that use g_matlab_escape_add() with calls to
 * g_matlab_escape_init() and g_matlab_escape_clear().  Be sure to call
 * g_matlab_escape_clear() <emphasis>before</emphasis> g_matlab_cleanup()
 * at the end of the call sequence.
 */
void g_matlab_escape_init()
{
  if (escape == NULL) return;
  g_hook_list_invoke(escape->data, FALSE);
  g_hook_list_clear(escape->data);
  g_hook_list_init(escape->data, sizeof(GHook));
}

/**
 * g_matlab_escape_add:
 * @func: cleanup function
 * @data: (allow-none): data to clean up
 *
 * Add an emergency release cleanup @func for @data if error occurs before
 * @data can be stored in its permanent location.  You must call
 * g_matlab_escape_init() in the given matlab entry function 
 * <emphasis>before</emphasis> calling g_matlab_escape_add(), and you must
 * call g_matlab_escape_clear() <emphasis>before</emphasis> calling 
 * g_matlab_cleanup() at the end of the function to prevent memory leaks and
 * premature freeing of memroy.
 *
 * In the event of an error, memory is freed in <emphasis>reverse</emphasis>
 * order.
 *
 * Returns: @data
 */
gpointer g_matlab_escape_add(GHookFunc func, gpointer data)
{
  if (data == NULL) return NULL;
  g_return_val_if_fail(func != NULL, NULL);
  if (escape == NULL) g_matlab_cleanup_push();
  GHook *hook = g_hook_alloc(escape->data);
  hook->func = func;
  hook->data = data;
  g_hook_prepend(escape->data, hook);
  return data;
}

/**
 * g_matlab_escape_clear_one:
 * @data: the data to remove
 *
 * Removes the callback for @data from the emergency cleanup list.
 */
void g_matlab_escape_clear_one(gpointer data)
{
  if (!data) return;
  g_return_if_fail(escape != NULL);
  GHook *hook = g_hook_find_data(escape->data, TRUE, data);
  if (hook) g_hook_destroy_link(escape->data, hook);
}

/**
 * g_matlab_escape_clear:
 *
 * Signal successful termination of the function and clear the emergency
 * cleanup callback list.  Must be called <emphasis>before</emphasis>
 * g_matlab_cleanup() at the very end of a function that uses
 * g_matlab_escape_add().
 */
void g_matlab_escape_clear()
{
  if (!escape) return;
  g_hook_list_clear(escape->data);
  g_hook_list_init(escape->data, sizeof(GHook));
}

/**
 * g_matlab_cleanup_push:
 *
 * Pushes a new hook list for cleanup and escape hooks onto the stack.  The
 * current list of cleanup and escape hooks will be reserved until it is 
 * restored by g_matlab_cleanup_pop().  Note that this function acts on
 * <emphasis>both</emphasis> the cleanup and escape lists.
 *
 * <emphasis>Important:</emphasis> The C-side call that includes a
 * g_matlab_cleanup_push()/g_matlab_cleanup_pop() pair must
 * <emphasis>not</emphasis> be interrupted before g_matlab_cleanup_pop().
 * Otherwise, all of the hooks (and the data they represent) in the current
 * cleanup and escape lists will leak.
 */
void g_matlab_cleanup_push()
{
  GHookList *hooks = g_slice_alloc0(sizeof(GHookList)*2);
  cleanup = g_slist_prepend(cleanup, hooks);
  escape = g_slist_prepend(escape, hooks+1);
  g_hook_list_init(cleanup->data, sizeof(GHook));
  g_hook_list_init(escape->data, sizeof(GHook));
}

/**
 * g_matlab_cleanup_pop:
 *
 * Clears the current cleanup and escape hooks then pops the hook list stack
 * to restore the previous cleanup and escape hook lists.  If there are no
 * other hook lists on the stack, the current lists are simply cleared.
 * Note: This calls g_matlab_escape_init(), so the caller must clear the
 * current escape list with g_matlab_escape_clear() if we are not in an escape
 * condition.
 */
void g_matlab_cleanup_pop()
{
  if (cleanup == NULL) return;
  g_matlab_cleanup();
  if (cleanup->next != NULL)
  {
    GHookList *discard = cleanup->data;
    cleanup = g_slist_remove(cleanup, discard);
    escape = g_slist_remove(escape, discard+1);
    g_slice_free1(sizeof(GHookList)*2, discard);
  }
}

/**
 * g_object_matlab_ensure:
 * @obj: (transfer none) (allow-none): a #GObject
 * @wrapper: the matlab wrapper for @obj
 *
 * Checks whether @obj has a matlab wrapper installed, and if not, installs
 * @wrapper.  Increases reference count.  Must be called before every matlab
 * call to C functions using obj, followed by a call to g_object_unref after
 * the function is complete.  This ensures that @obj has access to a matlab
 * wrapper, in the event that the C function increments the reference count
 * to @ref.
 */
void g_object_matlab_ensure(GObject *obj, mxArray *wrapper)
{
  if (obj == NULL) return;		// Can always get a wrappwer for NULL
  g_return_if_fail(G_IS_OBJECT(obj) && wrapper);
  TRACE("g_object_matlab_ensure(%ld)", obj);
  if (!g_object_get_qdata(obj, matlabWrapperKey))
  {
    mxArray *copy = mxDuplicateArray(wrapper);
    mexMakeArrayPersistent(copy);
    TRACE("g_object_matlab_ensure: Storing wrapper", NULL);
    g_object_set_qdata(obj, matlabWrapperKey, copy);
  }
  g_object_ref(obj);
}

/**
 * g_object_matlab_wrap:
 * @obj: (transfer none) (allow-none): a #GObject
 *
 * Fetches the matlab wrapper for @obj, if it exists.  Otherwise, create a
 * new matlab wrapper, add_toggle_ref, and cache the wrapper.
 *
 * Returns: the matlab wrapper for @obj.
 */
mxArray * g_object_matlab_wrap(GObject *obj)
{
  mxArray *wrapper, *copy, *args[3];
  if (obj == NULL)
  {
    // With no arguments, glib.GObject returns glib.GObject.NULL
    mexCallMATLAB(1, &wrapper, 0, NULL, "glib.GObject");
    return wrapper;
  }
  g_return_val_if_fail(G_IS_OBJECT(obj), NULL);

  if ((wrapper = g_object_get_qdata(obj, matlabWrapperKey)) != NULL)
  {
    TRACE("Fetching stored wrapper for %ld", obj);
    return mxDuplicateArray(wrapper);
  }
  
  TRACE("Wrapping %ld", obj);
  args[0] = mxCreateNumericMatrix(1, 1, MX_GLONG, mxREAL);
  ((GType*)mxGetData(args[0]))[0] = G_OBJECT_TYPE(obj);
  args[1] = mxCreateNumericMatrix(1, 1, MX_GPOINTER, mxREAL);
  ((gpointer*)mxGetData(args[1]))[0] = obj;
  args[2] = g_matlab_mxArray_from_gtype(G_OBJECT_TYPE(obj));
  mexCallMATLAB(1, &wrapper, 3, args, "glib.GObject");
  mxDestroyArray(args[0]);
  mxDestroyArray(args[1]);
  mxDestroyArray(args[2]);
  g_return_val_if_fail(wrapper != NULL, NULL);
  if (g_object_is_floating(obj)) g_object_ref_sink(obj);
  TRACE("g_object_add_toggle_ref: %ld", obj);
  g_object_add_toggle_ref(obj, g_object_matlab_toggle_notify, NULL);
  copy = mxDuplicateArray(wrapper);
  mexMakeArrayPersistent(copy);
  TRACE("Storing wrapper in %ld", obj);
  g_object_set_qdata(obj, matlabWrapperKey, copy);
  return wrapper;
}

/**
 * g_object_matlab_toggle_notify:
 * @data: unused
 * @obj: the #GObject
 * @is_last_ref: indicates that wrapper's is last remaining reference
 *
 * If @is_last_ref, the GObject's cached wrapper is released.  Otherwise,
 * the wrapper's reference is strong (i.e.  there are other C reference to
 * @obj)---do nothing so that other C functions returning the object can
 * find the wrapper.  Note, this function is never called directly by
 * matlab.
 */
static void g_object_matlab_toggle_notify(gpointer data, GObject *obj,
                                          gboolean is_last_ref)
{
  mxArray *wrapper;
  if (!is_last_ref) return;
  wrapper = g_object_get_qdata(obj, matlabWrapperKey);
  g_return_if_fail(wrapper != NULL);
  TRACE("toggle notify removing wrapper for %ld", obj);
  g_object_set_qdata(obj, matlabWrapperKey, NULL);
  mxDestroyArray(wrapper);
}

/**
 * g_object_matlab_remove_toggle_ref:
 * @obj: a #GObject
 *
 * Called when the last matlab reference to @obj is dropped and the matlab
 * wrapper is being destroyed.  Removes the wrapper's toggle_ref, and
 * @obj is subsequently destroyed.  (If there were C-side references to
 * @obj, then a reference to the matlab wrapper would be stored, and
 * thus the matlab wrapper would not be being destoyed.)
 */
void g_object_matlab_remove_toggle_ref(GObject *obj)
{
  g_return_if_fail(G_IS_OBJECT(obj));
  TRACE("Removing toggle ref for %ld", obj);
  g_object_remove_toggle_ref(obj, g_object_matlab_toggle_notify, NULL);
}
